feat: add manual known-word cache refresh path

- Add CLI command flag  with non-GUI dispatch flow and OSD error handling.

- Add runtime integration call and IPC hook so manual refresh works from command runner without app startup.

- Add public AnkiIntegration manual refresh API with force refresh semantics and guard reuse.

- Preserve default n+1 behavior by fixing config validation for malformed  values and adding tests.
This commit is contained in:
2026-02-15 00:03:38 -08:00
parent 66ac087f6f
commit 854b8fb6b6
17 changed files with 315 additions and 23 deletions

View File

@@ -16,11 +16,23 @@ SubMiner uses a service-oriented Electron architecture with a composition-orient
```text ```text
src/ src/
main.ts # Composition root — lifecycle wiring and state ownership main.ts # Entry point — delegates to src/main/ composition modules
preload.ts # Electron preload bridge preload.ts # Electron preload bridge
types.ts # Shared type definitions types.ts # Shared type definitions
main/ # Composition root modules (extracted from main.ts)
app-lifecycle.ts # Electron lifecycle event registration
cli-runtime.ts # CLI command handling and dispatch
dependencies.ts # Shared dependency builders for IPC/runtime
ipc-mpv-command.ts # MPV command composition helpers
ipc-runtime.ts # IPC channel registration and handlers
overlay-runtime.ts # Overlay window/modal selection and state
overlay-shortcuts-runtime.ts # Overlay keyboard shortcut handling
startup.ts # Startup bootstrap flow (argv/env processing)
startup-lifecycle.ts # App-ready initialization sequence
state.ts # Application runtime state container
subsync-runtime.ts # Subsync command orchestration
core/ core/
services/ # ~55 focused service modules (see below) services/ # ~60 focused service modules (see below)
utils/ # Pure helpers and coercion/config utilities utils/ # Pure helpers and coercion/config utilities
cli/ # CLI parsing and help output cli/ # CLI parsing and help output
config/ # Config schema, defaults, validation, template generation config/ # Config schema, defaults, validation, template generation
@@ -36,10 +48,10 @@ src/
### Service Layer (`src/core/services/`) ### Service Layer (`src/core/services/`)
- **Startup** — `startup-service`, `app-lifecycle-service` - **Startup** — `startup-service`, `app-lifecycle-service`, `app-ready-service`
- **Overlay** — `overlay-manager-service`, `overlay-window-service`, `overlay-visibility-service`, `overlay-bridge-service`, `overlay-runtime-init-service` - **Overlay** — `overlay-manager-service`, `overlay-window-service`, `overlay-visibility-service`, `overlay-bridge-service`, `overlay-runtime-init-service`, `overlay-content-measurement-service`
- **Shortcuts** — `shortcut-service`, `overlay-shortcut-service`, `overlay-shortcut-handler`, `shortcut-fallback-service`, `numeric-shortcut-service` - **Shortcuts** — `shortcut-service`, `overlay-shortcut-service`, `overlay-shortcut-handler`, `shortcut-fallback-service`, `numeric-shortcut-service`, `numeric-shortcut-session-service`
- **MPV** — `mpv-service`, `mpv-control-service`, `mpv-render-metrics-service` - **MPV** — `mpv-service`, `mpv-control-service`, `mpv-render-metrics-service`, `mpv-transport`, `mpv-protocol`, `mpv-state`, `mpv-properties`
- **IPC** — `ipc-service`, `ipc-command-service`, `runtime-options-ipc-service` - **IPC** — `ipc-service`, `ipc-command-service`, `runtime-options-ipc-service`
- **Mining** — `mining-service`, `field-grouping-service`, `field-grouping-overlay-service`, `anki-jimaku-service`, `anki-jimaku-ipc-service` - **Mining** — `mining-service`, `field-grouping-service`, `field-grouping-overlay-service`, `anki-jimaku-service`, `anki-jimaku-ipc-service`
- **Subtitles** — `subtitle-ws-service`, `subtitle-position-service`, `secondary-subtitle-service`, `tokenizer-service` - **Subtitles** — `subtitle-ws-service`, `subtitle-position-service`, `secondary-subtitle-service`, `tokenizer-service`

View File

@@ -0,0 +1,154 @@
import test from "node:test";
import assert from "node:assert/strict";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { AnkiIntegration } from "./anki-integration";
interface IntegrationTestContext {
integration: AnkiIntegration;
calls: {
findNotes: number;
notesInfo: number;
};
stateDir: string;
}
function createIntegrationTestContext(
options: {
highlightEnabled?: boolean;
onFindNotes?: () => Promise<number[]>;
onNotesInfo?: () => Promise<unknown[]>;
stateDirPrefix?: string;
} = {},
): IntegrationTestContext {
const calls = {
findNotes: 0,
notesInfo: 0,
};
const stateDir = fs.mkdtempSync(
path.join(os.tmpdir(), options.stateDirPrefix ?? "subminer-anki-integration-"),
);
const knownWordCacheStatePath = path.join(stateDir, "known-words-cache.json");
const client = {
findNotes: async () => {
calls.findNotes += 1;
if (options.onFindNotes) {
return options.onFindNotes();
}
return [] as number[];
},
notesInfo: async () => {
calls.notesInfo += 1;
if (options.onNotesInfo) {
return options.onNotesInfo();
}
return [] as unknown[];
},
} as {
findNotes: () => Promise<number[]>;
notesInfo: () => Promise<unknown[]>;
};
const integration = new AnkiIntegration(
{
nPlusOne: {
highlightEnabled: options.highlightEnabled ?? true,
},
},
{} as never,
{} as never,
undefined,
undefined,
undefined,
knownWordCacheStatePath,
);
const integrationWithClient = integration as unknown as {
client: {
findNotes: () => Promise<number[]>;
notesInfo: () => Promise<unknown[]>;
};
};
integrationWithClient.client = client;
const privateState = integration as unknown as {
knownWordsScope: string;
knownWordsLastRefreshedAtMs: number;
};
privateState.knownWordsScope = "is:note";
privateState.knownWordsLastRefreshedAtMs = Date.now();
return {
integration,
calls,
stateDir,
};
}
function cleanupIntegrationTestContext(ctx: IntegrationTestContext): void {
fs.rmSync(ctx.stateDir, { recursive: true, force: true });
}
test("AnkiIntegration.refreshKnownWordCache bypasses stale checks", async () => {
const ctx = createIntegrationTestContext();
try {
await ctx.integration.refreshKnownWordCache();
assert.equal(ctx.calls.findNotes, 1);
assert.equal(ctx.calls.notesInfo, 0);
} finally {
cleanupIntegrationTestContext(ctx);
}
});
test("AnkiIntegration.refreshKnownWordCache skips work when highlight mode is disabled", async () => {
const ctx = createIntegrationTestContext({
highlightEnabled: false,
stateDirPrefix: "subminer-anki-integration-disabled-",
});
try {
await ctx.integration.refreshKnownWordCache();
assert.equal(ctx.calls.findNotes, 0);
assert.equal(ctx.calls.notesInfo, 0);
} finally {
cleanupIntegrationTestContext(ctx);
}
});
test("AnkiIntegration.refreshKnownWordCache deduplicates concurrent refreshes", async () => {
let releaseFindNotes: (() => void) | undefined;
const findNotesPromise = new Promise<void>((resolve) => {
releaseFindNotes = resolve;
});
const ctx = createIntegrationTestContext({
onFindNotes: async () => {
await findNotesPromise;
return [] as number[];
},
stateDirPrefix: "subminer-anki-integration-concurrent-",
});
const first = ctx.integration.refreshKnownWordCache();
await Promise.resolve();
const second = ctx.integration.refreshKnownWordCache();
if (releaseFindNotes !== undefined) {
releaseFindNotes();
}
await Promise.all([first, second]);
try {
assert.equal(ctx.calls.findNotes, 1);
assert.equal(ctx.calls.notesInfo, 0);
} finally {
cleanupIntegrationTestContext(ctx);
}
});

View File

@@ -223,7 +223,11 @@ export class AnkiIntegration {
} }
} }
private async refreshKnownWords(): Promise<void> { async refreshKnownWordCache(): Promise<void> {
return this.refreshKnownWords(true);
}
private async refreshKnownWords(force = false): Promise<void> {
if (!this.isKnownWordCacheEnabled()) { if (!this.isKnownWordCacheEnabled()) {
log.debug("Known-word cache refresh skipped; feature disabled"); log.debug("Known-word cache refresh skipped; feature disabled");
return; return;
@@ -232,7 +236,7 @@ export class AnkiIntegration {
log.debug("Known-word cache refresh skipped; already refreshing"); log.debug("Known-word cache refresh skipped; already refreshing");
return; return;
} }
if (!this.isKnownWordCacheStale()) { if (!force && !this.isKnownWordCacheStale()) {
log.debug("Known-word cache refresh skipped; cache is fresh"); log.debug("Known-word cache refresh skipped; cache is fresh");
return; return;
} }

View File

@@ -41,4 +41,9 @@ test("hasExplicitCommand and shouldStartApp preserve command intent", () => {
const noCommand = parseArgs(["--verbose"]); const noCommand = parseArgs(["--verbose"]);
assert.equal(hasExplicitCommand(noCommand), false); assert.equal(hasExplicitCommand(noCommand), false);
assert.equal(shouldStartApp(noCommand), false); assert.equal(shouldStartApp(noCommand), false);
const refreshKnownWords = parseArgs(["--refresh-known-words"]);
assert.equal(refreshKnownWords.help, false);
assert.equal(hasExplicitCommand(refreshKnownWords), true);
assert.equal(shouldStartApp(refreshKnownWords), false);
}); });

View File

@@ -16,6 +16,7 @@ export interface CliArgs {
mineSentence: boolean; mineSentence: boolean;
mineSentenceMultiple: boolean; mineSentenceMultiple: boolean;
updateLastCardFromClipboard: boolean; updateLastCardFromClipboard: boolean;
refreshKnownWords: boolean;
toggleSecondarySub: boolean; toggleSecondarySub: boolean;
triggerFieldGrouping: boolean; triggerFieldGrouping: boolean;
triggerSubsync: boolean; triggerSubsync: boolean;
@@ -55,6 +56,7 @@ export function parseArgs(argv: string[]): CliArgs {
mineSentence: false, mineSentence: false,
mineSentenceMultiple: false, mineSentenceMultiple: false,
updateLastCardFromClipboard: false, updateLastCardFromClipboard: false,
refreshKnownWords: false,
toggleSecondarySub: false, toggleSecondarySub: false,
triggerFieldGrouping: false, triggerFieldGrouping: false,
triggerSubsync: false, triggerSubsync: false,
@@ -100,6 +102,7 @@ export function parseArgs(argv: string[]): CliArgs {
else if (arg === "--mine-sentence-multiple") args.mineSentenceMultiple = true; else if (arg === "--mine-sentence-multiple") args.mineSentenceMultiple = true;
else if (arg === "--update-last-card-from-clipboard") else if (arg === "--update-last-card-from-clipboard")
args.updateLastCardFromClipboard = true; args.updateLastCardFromClipboard = true;
else if (arg === "--refresh-known-words") args.refreshKnownWords = true;
else if (arg === "--toggle-secondary-sub") args.toggleSecondarySub = true; else if (arg === "--toggle-secondary-sub") args.toggleSecondarySub = true;
else if (arg === "--trigger-field-grouping") else if (arg === "--trigger-field-grouping")
args.triggerFieldGrouping = true; args.triggerFieldGrouping = true;
@@ -181,6 +184,7 @@ export function hasExplicitCommand(args: CliArgs): boolean {
args.mineSentence || args.mineSentence ||
args.mineSentenceMultiple || args.mineSentenceMultiple ||
args.updateLastCardFromClipboard || args.updateLastCardFromClipboard ||
args.refreshKnownWords ||
args.toggleSecondarySub || args.toggleSecondarySub ||
args.triggerFieldGrouping || args.triggerFieldGrouping ||
args.triggerSubsync || args.triggerSubsync ||

View File

@@ -17,4 +17,5 @@ test("printHelp includes configured texthooker port", () => {
assert.match(output, /--help\s+Show this help/); assert.match(output, /--help\s+Show this help/);
assert.match(output, /default: 7777/); assert.match(output, /default: 7777/);
assert.match(output, /--refresh-known-words/);
}); });

View File

@@ -19,6 +19,7 @@ SubMiner CLI commands:
--mine-sentence Mine sentence card from current subtitle --mine-sentence Mine sentence card from current subtitle
--mine-sentence-multiple Start multi-mine sentence mode --mine-sentence-multiple Start multi-mine sentence mode
--update-last-card-from-clipboard Update last card from clipboard --update-last-card-from-clipboard Update last card from clipboard
--refresh-known-words Refresh known words cache now
--toggle-secondary-sub Cycle secondary subtitle mode --toggle-secondary-sub Cycle secondary subtitle mode
--trigger-field-grouping Trigger Kiku field grouping --trigger-field-grouping Trigger Kiku field grouping
--trigger-subsync Run subtitle sync --trigger-subsync Run subtitle sync

View File

@@ -445,9 +445,14 @@ export class ConfigService {
: isObject(ac.openRouter) : isObject(ac.openRouter)
? ac.openRouter ? ac.openRouter
: {}; : {};
const { nPlusOne: _nPlusOneConfigFromAnkiConnect, ...ankiConnectWithoutNPlusOne } =
ac as Record<string, unknown>;
resolved.ankiConnect = { resolved.ankiConnect = {
...resolved.ankiConnect, ...resolved.ankiConnect,
...(isObject(ac) ? (ac as Partial<ResolvedConfig["ankiConnect"]>) : {}), ...(isObject(ankiConnectWithoutNPlusOne)
? (ankiConnectWithoutNPlusOne as Partial<ResolvedConfig["ankiConnect"]>)
: {}),
fields: { fields: {
...resolved.ankiConnect.fields, ...resolved.ankiConnect.fields,
...(isObject(ac.fields) ...(isObject(ac.fields)
@@ -606,6 +611,15 @@ export class ConfigService {
DEFAULT_CONFIG.ankiConnect.nPlusOne.highlightEnabled, DEFAULT_CONFIG.ankiConnect.nPlusOne.highlightEnabled,
"Legacy key is deprecated; use ankiConnect.nPlusOne.highlightEnabled", "Legacy key is deprecated; use ankiConnect.nPlusOne.highlightEnabled",
); );
} else if (nPlusOneConfig.highlightEnabled !== undefined) {
warn(
"ankiConnect.nPlusOne.highlightEnabled",
nPlusOneConfig.highlightEnabled,
resolved.ankiConnect.nPlusOne.highlightEnabled,
"Expected boolean.",
);
resolved.ankiConnect.nPlusOne.highlightEnabled =
DEFAULT_CONFIG.ankiConnect.nPlusOne.highlightEnabled;
} else { } else {
resolved.ankiConnect.nPlusOne.highlightEnabled = resolved.ankiConnect.nPlusOne.highlightEnabled =
DEFAULT_CONFIG.ankiConnect.nPlusOne.highlightEnabled; DEFAULT_CONFIG.ankiConnect.nPlusOne.highlightEnabled;
@@ -734,6 +748,7 @@ export class ConfigService {
resolved.ankiConnect.nPlusOne.decks, resolved.ankiConnect.nPlusOne.decks,
"Expected an array of strings.", "Expected an array of strings.",
); );
resolved.ankiConnect.nPlusOne.decks = [];
} }
if ( if (

View File

@@ -19,6 +19,7 @@ import {
export interface AnkiJimakuIpcDeps { export interface AnkiJimakuIpcDeps {
setAnkiConnectEnabled: (enabled: boolean) => void; setAnkiConnectEnabled: (enabled: boolean) => void;
clearAnkiHistory: () => void; clearAnkiHistory: () => void;
refreshKnownWords: () => Promise<void> | void;
respondFieldGrouping: (choice: KikuFieldGroupingChoice) => void; respondFieldGrouping: (choice: KikuFieldGroupingChoice) => void;
buildKikuMergePreview: ( buildKikuMergePreview: (
request: KikuMergePreviewRequest, request: KikuMergePreviewRequest,
@@ -55,6 +56,10 @@ export function registerAnkiJimakuIpcHandlers(
deps.clearAnkiHistory(); deps.clearAnkiHistory();
}); });
ipcMain.on("anki:refresh-known-words", async () => {
await deps.refreshKnownWords();
});
ipcMain.on( ipcMain.on(
"kiku:field-grouping-respond", "kiku:field-grouping-respond",
(_event: IpcMainEvent, choice: KikuFieldGroupingChoice) => { (_event: IpcMainEvent, choice: KikuFieldGroupingChoice) => {

View File

@@ -107,6 +107,7 @@ test("registerAnkiJimakuIpcRuntimeService provides full handler surface", () =>
const expected = [ const expected = [
"setAnkiConnectEnabled", "setAnkiConnectEnabled",
"clearAnkiHistory", "clearAnkiHistory",
"refreshKnownWords",
"respondFieldGrouping", "respondFieldGrouping",
"buildKikuMergePreview", "buildKikuMergePreview",
"getJimakuMediaInfo", "getJimakuMediaInfo",
@@ -124,6 +125,31 @@ test("registerAnkiJimakuIpcRuntimeService provides full handler surface", () =>
} }
}); });
test("refreshKnownWords throws when integration is unavailable", async () => {
const { registered } = createHarness();
await assert.rejects(
async () => {
await registered.refreshKnownWords();
},
{ message: "AnkiConnect integration not enabled" },
);
});
test("refreshKnownWords delegates to integration", async () => {
const { registered, state } = createHarness();
let refreshed = 0;
state.ankiIntegration = {
refreshKnownWordCache: async () => {
refreshed += 1;
},
};
await registered.refreshKnownWords();
assert.equal(refreshed, 1);
});
test("setAnkiConnectEnabled disables active integration and broadcasts changes", () => { test("setAnkiConnectEnabled disables active integration and broadcasts changes", () => {
const { registered, state } = createHarness(); const { registered, state } = createHarness();
let destroyed = 0; let destroyed = 0;

View File

@@ -10,7 +10,11 @@ import {
KikuFieldGroupingRequestData, KikuFieldGroupingRequestData,
} from "../../types"; } from "../../types";
import { sortJimakuFiles } from "../../jimaku/utils"; import { sortJimakuFiles } from "../../jimaku/utils";
import { registerAnkiJimakuIpcHandlers } from "./anki-jimaku-ipc-service"; import type { AnkiJimakuIpcDeps } from "./anki-jimaku-ipc-service";
export type RegisterAnkiJimakuIpcRuntimeHandler = (
deps: AnkiJimakuIpcDeps,
) => void;
interface MpvClientLike { interface MpvClientLike {
connected: boolean; connected: boolean;
@@ -60,7 +64,7 @@ export interface AnkiJimakuIpcRuntimeOptions {
export function registerAnkiJimakuIpcRuntimeService( export function registerAnkiJimakuIpcRuntimeService(
options: AnkiJimakuIpcRuntimeOptions, options: AnkiJimakuIpcRuntimeOptions,
registerHandlers: typeof registerAnkiJimakuIpcHandlers = registerAnkiJimakuIpcHandlers, registerHandlers: RegisterAnkiJimakuIpcRuntimeHandler,
): void { ): void {
registerHandlers({ registerHandlers({
setAnkiConnectEnabled: (enabled) => { setAnkiConnectEnabled: (enabled) => {
@@ -108,6 +112,13 @@ export function registerAnkiJimakuIpcRuntimeService(
console.log("AnkiConnect subtitle timing history cleared"); console.log("AnkiConnect subtitle timing history cleared");
} }
}, },
refreshKnownWords: async () => {
const integration = options.getAnkiIntegration();
if (!integration) {
throw new Error("AnkiConnect integration not enabled");
}
await integration.refreshKnownWordCache();
},
respondFieldGrouping: (choice) => { respondFieldGrouping: (choice) => {
const resolver = options.getFieldGroupingResolver(); const resolver = options.getFieldGroupingResolver();
if (resolver) { if (resolver) {

View File

@@ -26,6 +26,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
triggerFieldGrouping: false, triggerFieldGrouping: false,
triggerSubsync: false, triggerSubsync: false,
markAudioCard: false, markAudioCard: false,
refreshKnownWords: false,
openRuntimeOptions: false, openRuntimeOptions: false,
texthooker: false, texthooker: false,
help: false, help: false,
@@ -106,6 +107,9 @@ function createDeps(overrides: Partial<CliCommandServiceDeps> = {}) {
updateLastCardFromClipboard: async () => { updateLastCardFromClipboard: async () => {
calls.push("updateLastCardFromClipboard"); calls.push("updateLastCardFromClipboard");
}, },
refreshKnownWords: async () => {
calls.push("refreshKnownWords");
},
cycleSecondarySubMode: () => { cycleSecondarySubMode: () => {
calls.push("cycleSecondarySubMode"); calls.push("cycleSecondarySubMode");
}, },
@@ -288,3 +292,27 @@ test("handleCliCommandService handles visibility and utility command dispatches"
); );
} }
}); });
test("handleCliCommandService runs refresh-known-words command", () => {
const { deps, calls } = createDeps();
handleCliCommandService(makeArgs({ refreshKnownWords: true }), "initial", deps);
assert.ok(calls.includes("refreshKnownWords"));
});
test("handleCliCommandService reports async refresh-known-words errors to OSD", async () => {
const { deps, calls, osd } = createDeps({
refreshKnownWords: async () => {
throw new Error("refresh boom");
},
});
handleCliCommandService(makeArgs({ refreshKnownWords: true }), "initial", deps);
await new Promise((resolve) => setImmediate(resolve));
assert.ok(
calls.some((value) => value.startsWith("error:refreshKnownWords failed:")),
);
assert.ok(osd.some((value) => value.includes("Refresh known words failed: refresh boom")));
});

View File

@@ -29,6 +29,7 @@ export interface CliCommandServiceDeps {
mineSentenceCard: () => Promise<void>; mineSentenceCard: () => Promise<void>;
startPendingMineSentenceMultiple: (timeoutMs: number) => void; startPendingMineSentenceMultiple: (timeoutMs: number) => void;
updateLastCardFromClipboard: () => Promise<void>; updateLastCardFromClipboard: () => Promise<void>;
refreshKnownWords: () => Promise<void>;
cycleSecondarySubMode: () => void; cycleSecondarySubMode: () => void;
triggerFieldGrouping: () => Promise<void>; triggerFieldGrouping: () => Promise<void>;
triggerSubsyncFromConfig: () => Promise<void>; triggerSubsyncFromConfig: () => Promise<void>;
@@ -83,6 +84,7 @@ interface MiningCliRuntime {
mineSentenceCard: () => Promise<void>; mineSentenceCard: () => Promise<void>;
startPendingMineSentenceMultiple: (timeoutMs: number) => void; startPendingMineSentenceMultiple: (timeoutMs: number) => void;
updateLastCardFromClipboard: () => Promise<void>; updateLastCardFromClipboard: () => Promise<void>;
refreshKnownWords: () => Promise<void>;
triggerFieldGrouping: () => Promise<void>; triggerFieldGrouping: () => Promise<void>;
triggerSubsyncFromConfig: () => Promise<void>; triggerSubsyncFromConfig: () => Promise<void>;
markLastCardAsAudioCard: () => Promise<void>; markLastCardAsAudioCard: () => Promise<void>;
@@ -159,6 +161,7 @@ export function createCliCommandDepsRuntimeService(
startPendingMineSentenceMultiple: startPendingMineSentenceMultiple:
options.mining.startPendingMineSentenceMultiple, options.mining.startPendingMineSentenceMultiple,
updateLastCardFromClipboard: options.mining.updateLastCardFromClipboard, updateLastCardFromClipboard: options.mining.updateLastCardFromClipboard,
refreshKnownWords: options.mining.refreshKnownWords,
cycleSecondarySubMode: options.ui.cycleSecondarySubMode, cycleSecondarySubMode: options.ui.cycleSecondarySubMode,
triggerFieldGrouping: options.mining.triggerFieldGrouping, triggerFieldGrouping: options.mining.triggerFieldGrouping,
triggerSubsyncFromConfig: options.mining.triggerSubsyncFromConfig, triggerSubsyncFromConfig: options.mining.triggerSubsyncFromConfig,
@@ -208,6 +211,7 @@ export function handleCliCommandService(
args.mineSentence || args.mineSentence ||
args.mineSentenceMultiple || args.mineSentenceMultiple ||
args.updateLastCardFromClipboard || args.updateLastCardFromClipboard ||
args.refreshKnownWords ||
args.toggleSecondarySub || args.toggleSecondarySub ||
args.triggerFieldGrouping || args.triggerFieldGrouping ||
args.triggerSubsync || args.triggerSubsync ||
@@ -295,6 +299,13 @@ export function handleCliCommandService(
"updateLastCardFromClipboard", "updateLastCardFromClipboard",
"Update failed", "Update failed",
); );
} else if (args.refreshKnownWords) {
runAsyncWithOsd(
() => deps.refreshKnownWords(),
deps,
"refreshKnownWords",
"Refresh known words failed",
);
} else if (args.toggleSecondarySub) { } else if (args.toggleSecondarySub) {
deps.cycleSecondarySubMode(); deps.cycleSecondarySubMode();
} else if (args.triggerFieldGrouping) { } else if (args.triggerFieldGrouping) {

View File

@@ -704,6 +704,7 @@ function handleCliCommand(
startPendingMineSentenceMultiple: (timeoutMs: number) => startPendingMineSentenceMultiple: (timeoutMs: number) =>
startPendingMineSentenceMultiple(timeoutMs), startPendingMineSentenceMultiple(timeoutMs),
updateLastCardFromClipboard: () => updateLastCardFromClipboard(), updateLastCardFromClipboard: () => updateLastCardFromClipboard(),
refreshKnownWordCache: () => refreshKnownWordCache(),
triggerFieldGrouping: () => triggerFieldGrouping(), triggerFieldGrouping: () => triggerFieldGrouping(),
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(), triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
markLastCardAsAudioCard: () => markLastCardAsAudioCard(), markLastCardAsAudioCard: () => markLastCardAsAudioCard(),
@@ -1110,6 +1111,14 @@ async function updateLastCardFromClipboard(): Promise<void> {
); );
} }
async function refreshKnownWordCache(): Promise<void> {
if (!appState.ankiIntegration) {
throw new Error("AnkiConnect integration not enabled");
}
await appState.ankiIntegration.refreshKnownWordCache();
}
async function triggerFieldGrouping(): Promise<void> { async function triggerFieldGrouping(): Promise<void> {
await triggerFieldGroupingService( await triggerFieldGroupingService(
{ {

View File

@@ -22,6 +22,7 @@ export interface CliCommandRuntimeServiceContext {
mineSentenceCard: () => Promise<void>; mineSentenceCard: () => Promise<void>;
startPendingMineSentenceMultiple: (timeoutMs: number) => void; startPendingMineSentenceMultiple: (timeoutMs: number) => void;
updateLastCardFromClipboard: () => Promise<void>; updateLastCardFromClipboard: () => Promise<void>;
refreshKnownWordCache: () => Promise<void>;
triggerFieldGrouping: () => Promise<void>; triggerFieldGrouping: () => Promise<void>;
triggerSubsyncFromConfig: () => Promise<void>; triggerSubsyncFromConfig: () => Promise<void>;
markLastCardAsAudioCard: () => Promise<void>; markLastCardAsAudioCard: () => Promise<void>;
@@ -73,6 +74,7 @@ function createCliCommandDepsFromContext(
mineSentenceCard: context.mineSentenceCard, mineSentenceCard: context.mineSentenceCard,
startPendingMineSentenceMultiple: context.startPendingMineSentenceMultiple, startPendingMineSentenceMultiple: context.startPendingMineSentenceMultiple,
updateLastCardFromClipboard: context.updateLastCardFromClipboard, updateLastCardFromClipboard: context.updateLastCardFromClipboard,
refreshKnownWords: context.refreshKnownWordCache,
triggerFieldGrouping: context.triggerFieldGrouping, triggerFieldGrouping: context.triggerFieldGrouping,
triggerSubsyncFromConfig: context.triggerSubsyncFromConfig, triggerSubsyncFromConfig: context.triggerSubsyncFromConfig,
markLastCardAsAudioCard: context.markLastCardAsAudioCard, markLastCardAsAudioCard: context.markLastCardAsAudioCard,

View File

@@ -145,6 +145,7 @@ export interface CliCommandRuntimeServiceDepsParams {
CliCommandDepsRuntimeOptions["mining"]["startPendingMineSentenceMultiple"]; CliCommandDepsRuntimeOptions["mining"]["startPendingMineSentenceMultiple"];
updateLastCardFromClipboard: updateLastCardFromClipboard:
CliCommandDepsRuntimeOptions["mining"]["updateLastCardFromClipboard"]; CliCommandDepsRuntimeOptions["mining"]["updateLastCardFromClipboard"];
refreshKnownWords: CliCommandDepsRuntimeOptions["mining"]["refreshKnownWords"];
triggerFieldGrouping: CliCommandDepsRuntimeOptions["mining"]["triggerFieldGrouping"]; triggerFieldGrouping: CliCommandDepsRuntimeOptions["mining"]["triggerFieldGrouping"];
triggerSubsyncFromConfig: triggerSubsyncFromConfig:
CliCommandDepsRuntimeOptions["mining"]["triggerSubsyncFromConfig"]; CliCommandDepsRuntimeOptions["mining"]["triggerSubsyncFromConfig"];
@@ -273,6 +274,7 @@ export function createCliCommandRuntimeServiceDeps(
mineSentenceCard: params.mining.mineSentenceCard, mineSentenceCard: params.mining.mineSentenceCard,
startPendingMineSentenceMultiple: params.mining.startPendingMineSentenceMultiple, startPendingMineSentenceMultiple: params.mining.startPendingMineSentenceMultiple,
updateLastCardFromClipboard: params.mining.updateLastCardFromClipboard, updateLastCardFromClipboard: params.mining.updateLastCardFromClipboard,
refreshKnownWords: params.mining.refreshKnownWords,
triggerFieldGrouping: params.mining.triggerFieldGrouping, triggerFieldGrouping: params.mining.triggerFieldGrouping,
triggerSubsyncFromConfig: params.mining.triggerSubsyncFromConfig, triggerSubsyncFromConfig: params.mining.triggerSubsyncFromConfig,
markLastCardAsAudioCard: params.mining.markLastCardAsAudioCard, markLastCardAsAudioCard: params.mining.markLastCardAsAudioCard,

View File

@@ -3,6 +3,7 @@ import {
registerAnkiJimakuIpcRuntimeService, registerAnkiJimakuIpcRuntimeService,
registerIpcHandlersService, registerIpcHandlersService,
} from "../core/services"; } from "../core/services";
import { registerAnkiJimakuIpcHandlers } from "../core/services/anki-jimaku-ipc-service";
import { import {
createAnkiJimakuIpcRuntimeServiceDeps, createAnkiJimakuIpcRuntimeServiceDeps,
AnkiJimakuIpcRuntimeServiceDepsParams, AnkiJimakuIpcRuntimeServiceDepsParams,
@@ -34,6 +35,7 @@ export function registerAnkiJimakuIpcRuntimeServices(
): void { ): void {
registerAnkiJimakuIpcRuntimeService( registerAnkiJimakuIpcRuntimeService(
createAnkiJimakuIpcRuntimeServiceDeps(params), createAnkiJimakuIpcRuntimeServiceDeps(params),
registerAnkiJimakuIpcHandlers,
); );
} }