mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
refactor: add main.ts decomposition guardrails and extract core helpers
This commit is contained in:
44
src/cli/args.test.ts
Normal file
44
src/cli/args.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { hasExplicitCommand, parseArgs, shouldStartApp } from "./args";
|
||||
|
||||
test("parseArgs parses booleans and value flags", () => {
|
||||
const args = parseArgs([
|
||||
"--start",
|
||||
"--socket",
|
||||
"/tmp/mpv.sock",
|
||||
"--backend=hyprland",
|
||||
"--port",
|
||||
"6000",
|
||||
"--log-level",
|
||||
"warn",
|
||||
"--verbose",
|
||||
]);
|
||||
|
||||
assert.equal(args.start, true);
|
||||
assert.equal(args.socketPath, "/tmp/mpv.sock");
|
||||
assert.equal(args.backend, "hyprland");
|
||||
assert.equal(args.texthookerPort, 6000);
|
||||
assert.equal(args.logLevel, "warn");
|
||||
assert.equal(args.verbose, true);
|
||||
});
|
||||
|
||||
test("parseArgs ignores missing value after --log-level", () => {
|
||||
const args = parseArgs(["--log-level", "--start"]);
|
||||
assert.equal(args.logLevel, undefined);
|
||||
assert.equal(args.start, true);
|
||||
});
|
||||
|
||||
test("hasExplicitCommand and shouldStartApp preserve command intent", () => {
|
||||
const stopOnly = parseArgs(["--stop"]);
|
||||
assert.equal(hasExplicitCommand(stopOnly), true);
|
||||
assert.equal(shouldStartApp(stopOnly), false);
|
||||
|
||||
const toggle = parseArgs(["--toggle-visible-overlay"]);
|
||||
assert.equal(hasExplicitCommand(toggle), true);
|
||||
assert.equal(shouldStartApp(toggle), true);
|
||||
|
||||
const noCommand = parseArgs(["--verbose"]);
|
||||
assert.equal(hasExplicitCommand(noCommand), false);
|
||||
assert.equal(shouldStartApp(noCommand), false);
|
||||
});
|
||||
241
src/cli/args.ts
Normal file
241
src/cli/args.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
export interface CliArgs {
|
||||
start: boolean;
|
||||
stop: boolean;
|
||||
toggle: boolean;
|
||||
toggleVisibleOverlay: boolean;
|
||||
toggleInvisibleOverlay: boolean;
|
||||
settings: boolean;
|
||||
show: boolean;
|
||||
hide: boolean;
|
||||
showVisibleOverlay: boolean;
|
||||
hideVisibleOverlay: boolean;
|
||||
showInvisibleOverlay: boolean;
|
||||
hideInvisibleOverlay: boolean;
|
||||
copySubtitle: boolean;
|
||||
copySubtitleMultiple: boolean;
|
||||
mineSentence: boolean;
|
||||
mineSentenceMultiple: boolean;
|
||||
updateLastCardFromClipboard: boolean;
|
||||
toggleSecondarySub: boolean;
|
||||
triggerFieldGrouping: boolean;
|
||||
triggerSubsync: boolean;
|
||||
markAudioCard: boolean;
|
||||
openRuntimeOptions: boolean;
|
||||
texthooker: boolean;
|
||||
help: boolean;
|
||||
autoStartOverlay: boolean;
|
||||
generateConfig: boolean;
|
||||
configPath?: string;
|
||||
backupOverwrite: boolean;
|
||||
socketPath?: string;
|
||||
backend?: string;
|
||||
texthookerPort?: number;
|
||||
verbose: boolean;
|
||||
logLevel?: "debug" | "info" | "warn" | "error";
|
||||
}
|
||||
|
||||
export type CliCommandSource = "initial" | "second-instance";
|
||||
|
||||
export function parseArgs(argv: string[]): CliArgs {
|
||||
const args: CliArgs = {
|
||||
start: false,
|
||||
stop: false,
|
||||
toggle: false,
|
||||
toggleVisibleOverlay: false,
|
||||
toggleInvisibleOverlay: false,
|
||||
settings: false,
|
||||
show: false,
|
||||
hide: false,
|
||||
showVisibleOverlay: false,
|
||||
hideVisibleOverlay: false,
|
||||
showInvisibleOverlay: false,
|
||||
hideInvisibleOverlay: false,
|
||||
copySubtitle: false,
|
||||
copySubtitleMultiple: false,
|
||||
mineSentence: false,
|
||||
mineSentenceMultiple: false,
|
||||
updateLastCardFromClipboard: false,
|
||||
toggleSecondarySub: false,
|
||||
triggerFieldGrouping: false,
|
||||
triggerSubsync: false,
|
||||
markAudioCard: false,
|
||||
openRuntimeOptions: false,
|
||||
texthooker: false,
|
||||
help: false,
|
||||
autoStartOverlay: false,
|
||||
generateConfig: false,
|
||||
backupOverwrite: false,
|
||||
verbose: false,
|
||||
};
|
||||
|
||||
const readValue = (value?: string): string | undefined => {
|
||||
if (!value) return undefined;
|
||||
if (value.startsWith("--")) return undefined;
|
||||
return value;
|
||||
};
|
||||
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (!arg.startsWith("--")) continue;
|
||||
|
||||
if (arg === "--start") args.start = true;
|
||||
else if (arg === "--stop") args.stop = true;
|
||||
else if (arg === "--toggle") args.toggle = true;
|
||||
else if (arg === "--toggle-visible-overlay")
|
||||
args.toggleVisibleOverlay = true;
|
||||
else if (arg === "--toggle-invisible-overlay")
|
||||
args.toggleInvisibleOverlay = true;
|
||||
else if (arg === "--settings" || arg === "--yomitan") args.settings = true;
|
||||
else if (arg === "--show") args.show = true;
|
||||
else if (arg === "--hide") args.hide = true;
|
||||
else if (arg === "--show-visible-overlay") args.showVisibleOverlay = true;
|
||||
else if (arg === "--hide-visible-overlay") args.hideVisibleOverlay = true;
|
||||
else if (arg === "--show-invisible-overlay")
|
||||
args.showInvisibleOverlay = true;
|
||||
else if (arg === "--hide-invisible-overlay")
|
||||
args.hideInvisibleOverlay = true;
|
||||
else if (arg === "--copy-subtitle") args.copySubtitle = true;
|
||||
else if (arg === "--copy-subtitle-multiple") args.copySubtitleMultiple = true;
|
||||
else if (arg === "--mine-sentence") args.mineSentence = true;
|
||||
else if (arg === "--mine-sentence-multiple") args.mineSentenceMultiple = true;
|
||||
else if (arg === "--update-last-card-from-clipboard")
|
||||
args.updateLastCardFromClipboard = true;
|
||||
else if (arg === "--toggle-secondary-sub") args.toggleSecondarySub = true;
|
||||
else if (arg === "--trigger-field-grouping")
|
||||
args.triggerFieldGrouping = true;
|
||||
else if (arg === "--trigger-subsync") args.triggerSubsync = true;
|
||||
else if (arg === "--mark-audio-card") args.markAudioCard = true;
|
||||
else if (arg === "--open-runtime-options") args.openRuntimeOptions = true;
|
||||
else if (arg === "--texthooker") args.texthooker = true;
|
||||
else if (arg === "--auto-start-overlay") args.autoStartOverlay = true;
|
||||
else if (arg === "--generate-config") args.generateConfig = true;
|
||||
else if (arg === "--backup-overwrite") args.backupOverwrite = true;
|
||||
else if (arg === "--help") args.help = true;
|
||||
else if (arg === "--verbose") args.verbose = true;
|
||||
else if (arg.startsWith("--log-level=")) {
|
||||
const value = arg.split("=", 2)[1]?.toLowerCase();
|
||||
if (
|
||||
value === "debug" ||
|
||||
value === "info" ||
|
||||
value === "warn" ||
|
||||
value === "error"
|
||||
) {
|
||||
args.logLevel = value;
|
||||
}
|
||||
} else if (arg === "--log-level") {
|
||||
const value = readValue(argv[i + 1])?.toLowerCase();
|
||||
if (
|
||||
value === "debug" ||
|
||||
value === "info" ||
|
||||
value === "warn" ||
|
||||
value === "error"
|
||||
) {
|
||||
args.logLevel = value;
|
||||
}
|
||||
} else if (arg.startsWith("--config-path=")) {
|
||||
const value = arg.split("=", 2)[1];
|
||||
if (value) args.configPath = value;
|
||||
} else if (arg === "--config-path") {
|
||||
const value = readValue(argv[i + 1]);
|
||||
if (value) args.configPath = value;
|
||||
} else if (arg.startsWith("--socket=")) {
|
||||
const value = arg.split("=", 2)[1];
|
||||
if (value) args.socketPath = value;
|
||||
} else if (arg === "--socket") {
|
||||
const value = readValue(argv[i + 1]);
|
||||
if (value) args.socketPath = value;
|
||||
} else if (arg.startsWith("--backend=")) {
|
||||
const value = arg.split("=", 2)[1];
|
||||
if (value) args.backend = value;
|
||||
} else if (arg === "--backend") {
|
||||
const value = readValue(argv[i + 1]);
|
||||
if (value) args.backend = value;
|
||||
} else if (arg.startsWith("--port=")) {
|
||||
const value = Number(arg.split("=", 2)[1]);
|
||||
if (!Number.isNaN(value)) args.texthookerPort = value;
|
||||
} else if (arg === "--port") {
|
||||
const value = Number(readValue(argv[i + 1]));
|
||||
if (!Number.isNaN(value)) args.texthookerPort = value;
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export function hasExplicitCommand(args: CliArgs): boolean {
|
||||
return (
|
||||
args.start ||
|
||||
args.stop ||
|
||||
args.toggle ||
|
||||
args.toggleVisibleOverlay ||
|
||||
args.toggleInvisibleOverlay ||
|
||||
args.settings ||
|
||||
args.show ||
|
||||
args.hide ||
|
||||
args.showVisibleOverlay ||
|
||||
args.hideVisibleOverlay ||
|
||||
args.showInvisibleOverlay ||
|
||||
args.hideInvisibleOverlay ||
|
||||
args.copySubtitle ||
|
||||
args.copySubtitleMultiple ||
|
||||
args.mineSentence ||
|
||||
args.mineSentenceMultiple ||
|
||||
args.updateLastCardFromClipboard ||
|
||||
args.toggleSecondarySub ||
|
||||
args.triggerFieldGrouping ||
|
||||
args.triggerSubsync ||
|
||||
args.markAudioCard ||
|
||||
args.openRuntimeOptions ||
|
||||
args.texthooker ||
|
||||
args.generateConfig ||
|
||||
args.help
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldStartApp(args: CliArgs): boolean {
|
||||
if (args.stop && !args.start) return false;
|
||||
if (
|
||||
args.start ||
|
||||
args.toggle ||
|
||||
args.toggleVisibleOverlay ||
|
||||
args.toggleInvisibleOverlay ||
|
||||
args.copySubtitle ||
|
||||
args.copySubtitleMultiple ||
|
||||
args.mineSentence ||
|
||||
args.mineSentenceMultiple ||
|
||||
args.updateLastCardFromClipboard ||
|
||||
args.toggleSecondarySub ||
|
||||
args.triggerFieldGrouping ||
|
||||
args.triggerSubsync ||
|
||||
args.markAudioCard ||
|
||||
args.openRuntimeOptions ||
|
||||
args.texthooker
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function commandNeedsOverlayRuntime(args: CliArgs): boolean {
|
||||
return (
|
||||
args.toggle ||
|
||||
args.toggleVisibleOverlay ||
|
||||
args.toggleInvisibleOverlay ||
|
||||
args.show ||
|
||||
args.hide ||
|
||||
args.showVisibleOverlay ||
|
||||
args.hideVisibleOverlay ||
|
||||
args.showInvisibleOverlay ||
|
||||
args.hideInvisibleOverlay ||
|
||||
args.copySubtitle ||
|
||||
args.copySubtitleMultiple ||
|
||||
args.mineSentence ||
|
||||
args.mineSentenceMultiple ||
|
||||
args.updateLastCardFromClipboard ||
|
||||
args.toggleSecondarySub ||
|
||||
args.triggerFieldGrouping ||
|
||||
args.triggerSubsync ||
|
||||
args.markAudioCard ||
|
||||
args.openRuntimeOptions
|
||||
);
|
||||
}
|
||||
20
src/cli/help.test.ts
Normal file
20
src/cli/help.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { printHelp } from "./help";
|
||||
|
||||
test("printHelp includes configured texthooker port", () => {
|
||||
const original = console.log;
|
||||
let output = "";
|
||||
console.log = (value?: unknown) => {
|
||||
output += String(value);
|
||||
};
|
||||
|
||||
try {
|
||||
printHelp(7777);
|
||||
} finally {
|
||||
console.log = original;
|
||||
}
|
||||
|
||||
assert.match(output, /--help\s+Show this help/);
|
||||
assert.match(output, /default: 7777/);
|
||||
});
|
||||
40
src/cli/help.ts
Normal file
40
src/cli/help.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export function printHelp(defaultTexthookerPort: number): void {
|
||||
console.log(`
|
||||
SubMiner CLI commands:
|
||||
--start Start MPV IPC connection and overlay control loop
|
||||
--stop Stop the running overlay app
|
||||
--toggle Toggle visible subtitle overlay visibility (legacy alias)
|
||||
--toggle-visible-overlay Toggle visible subtitle overlay visibility
|
||||
--toggle-invisible-overlay Toggle invisible interactive overlay visibility
|
||||
--settings Open Yomitan settings window
|
||||
--texthooker Launch texthooker only (no overlay window)
|
||||
--show Force show visible overlay (legacy alias)
|
||||
--hide Force hide visible overlay (legacy alias)
|
||||
--show-visible-overlay Force show visible subtitle overlay
|
||||
--hide-visible-overlay Force hide visible subtitle overlay
|
||||
--show-invisible-overlay Force show invisible interactive overlay
|
||||
--hide-invisible-overlay Force hide invisible interactive overlay
|
||||
--copy-subtitle Copy current subtitle text
|
||||
--copy-subtitle-multiple Start multi-copy mode
|
||||
--mine-sentence Mine sentence card from current subtitle
|
||||
--mine-sentence-multiple Start multi-mine sentence mode
|
||||
--update-last-card-from-clipboard Update last card from clipboard
|
||||
--toggle-secondary-sub Cycle secondary subtitle mode
|
||||
--trigger-field-grouping Trigger Kiku field grouping
|
||||
--trigger-subsync Run subtitle sync
|
||||
--mark-audio-card Mark last card as audio card
|
||||
--open-runtime-options Open runtime options palette
|
||||
--auto-start-overlay Auto-hide mpv subtitles on connect (show overlay)
|
||||
--socket PATH Override MPV IPC socket/pipe path
|
||||
--backend BACKEND Override window tracker backend (auto, hyprland, sway, x11, macos)
|
||||
--port PORT Texthooker server port (default: ${defaultTexthookerPort})
|
||||
--verbose Enable debug logging (equivalent to --log-level debug)
|
||||
--log-level LEVEL Set log level: debug, info, warn, error
|
||||
--generate-config Generate default config.jsonc from centralized config registry
|
||||
--config-path PATH Target config path for --generate-config
|
||||
--backup-overwrite With --generate-config, backup and overwrite existing file
|
||||
--dev Run in development mode
|
||||
--debug Alias for --dev
|
||||
--help Show this help
|
||||
`);
|
||||
}
|
||||
Reference in New Issue
Block a user