diff --git a/docs-site/configuration.md b/docs-site/configuration.md index d24a604..2667859 100644 --- a/docs-site/configuration.md +++ b/docs-site/configuration.md @@ -951,7 +951,7 @@ For GameSentenceMiner on Linux, the default overlay profile path is typically `~ | Option | Values | Description | | --------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `externalProfilePath` | string path | Optional absolute path to another app's Yomitan Electron profile. SubMiner loads that profile read-only and reuses its dictionaries/settings. | +| `externalProfilePath` | string path | Optional absolute path, or a path beginning with `~` (expanded to your home directory), to another app's Yomitan Electron profile. SubMiner loads that profile read-only and reuses its dictionaries/settings. | External-profile mode behavior: diff --git a/src/config/resolve/jellyfin.test.ts b/src/config/resolve/jellyfin.test.ts index eed36a1..5cc211d 100644 --- a/src/config/resolve/jellyfin.test.ts +++ b/src/config/resolve/jellyfin.test.ts @@ -1,6 +1,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import * as os from 'node:os'; +import * as path from 'node:path'; import { createResolveContext } from './context'; import { applyIntegrationConfig } from './integrations'; @@ -139,5 +140,8 @@ test('yomitan externalProfilePath expands leading tilde to the current home dire applyIntegrationConfig(context); - assert.equal(context.resolved.yomitan.externalProfilePath, `${homeDir}/.config/gsm_overlay`); + assert.equal( + context.resolved.yomitan.externalProfilePath, + path.join(homeDir, '.config', 'gsm_overlay'), + ); }); diff --git a/src/core/services/overlay-window-config.test.ts b/src/core/services/overlay-window-config.test.ts index 3bac186..5b2a467 100644 --- a/src/core/services/overlay-window-config.test.ts +++ b/src/core/services/overlay-window-config.test.ts @@ -1,18 +1,27 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import fs from 'node:fs'; -import path from 'node:path'; +import { buildOverlayWindowOptions } from './overlay-window-options'; test('overlay window config explicitly disables renderer sandbox for preload compatibility', () => { - const sourcePath = path.join(process.cwd(), 'src/core/services/overlay-window.ts'); - const source = fs.readFileSync(sourcePath, 'utf8'); + const options = buildOverlayWindowOptions('visible', { + isDev: false, + yomitanSession: null, + }); - assert.match(source, /webPreferences:\s*\{[\s\S]*sandbox:\s*false[\s\S]*\}/m); + assert.equal(options.webPreferences?.sandbox, false); }); test('overlay window config uses the provided Yomitan session when available', () => { - const sourcePath = path.join(process.cwd(), 'src/core/services/overlay-window.ts'); - const source = fs.readFileSync(sourcePath, 'utf8'); + const yomitanSession = { id: 'session' } as never; + const withSession = buildOverlayWindowOptions('visible', { + isDev: false, + yomitanSession, + }); + const withoutSession = buildOverlayWindowOptions('visible', { + isDev: false, + yomitanSession: null, + }); - assert.match(source, /session:\s*options\.yomitanSession\s*\?\?\s*undefined/); + assert.equal(withSession.webPreferences?.session, yomitanSession); + assert.equal(withoutSession.webPreferences?.session, undefined); }); diff --git a/src/core/services/overlay-window-options.ts b/src/core/services/overlay-window-options.ts new file mode 100644 index 0000000..80619b2 --- /dev/null +++ b/src/core/services/overlay-window-options.ts @@ -0,0 +1,39 @@ +import type { BrowserWindowConstructorOptions, Session } from 'electron'; +import * as path from 'path'; +import type { OverlayWindowKind } from './overlay-window-input'; + +export function buildOverlayWindowOptions( + kind: OverlayWindowKind, + options: { + isDev: boolean; + yomitanSession?: Session | null; + }, +): BrowserWindowConstructorOptions { + const showNativeDebugFrame = process.platform === 'win32' && options.isDev; + + return { + show: false, + width: 800, + height: 600, + x: 0, + y: 0, + transparent: true, + frame: false, + alwaysOnTop: true, + skipTaskbar: true, + resizable: false, + hasShadow: false, + focusable: true, + acceptFirstMouse: true, + ...(process.platform === 'win32' ? { thickFrame: showNativeDebugFrame } : {}), + webPreferences: { + preload: path.join(__dirname, '..', '..', 'preload.js'), + contextIsolation: true, + nodeIntegration: false, + sandbox: false, + webSecurity: true, + session: options.yomitanSession ?? undefined, + additionalArguments: [`--overlay-layer=${kind}`], + }, + }; +} diff --git a/src/core/services/overlay-window.ts b/src/core/services/overlay-window.ts index c7700c4..773b0f5 100644 --- a/src/core/services/overlay-window.ts +++ b/src/core/services/overlay-window.ts @@ -7,6 +7,7 @@ import { handleOverlayWindowBeforeInputEvent, type OverlayWindowKind, } from './overlay-window-input'; +import { buildOverlayWindowOptions } from './overlay-window-options'; const logger = createLogger('main:overlay-window'); const overlayWindowLayerByInstance = new WeakMap(); @@ -81,32 +82,7 @@ export function createOverlayWindow( yomitanSession?: Session | null; }, ): BrowserWindow { - const showNativeDebugFrame = process.platform === 'win32' && options.isDev; - const window = new BrowserWindow({ - show: false, - width: 800, - height: 600, - x: 0, - y: 0, - transparent: true, - frame: false, - alwaysOnTop: true, - skipTaskbar: true, - resizable: false, - hasShadow: false, - focusable: true, - acceptFirstMouse: true, - ...(process.platform === 'win32' ? { thickFrame: showNativeDebugFrame } : {}), - webPreferences: { - preload: path.join(__dirname, '..', '..', 'preload.js'), - contextIsolation: true, - nodeIntegration: false, - sandbox: false, - webSecurity: true, - session: options.yomitanSession ?? undefined, - additionalArguments: [`--overlay-layer=${kind}`], - }, - }); + const window = new BrowserWindow(buildOverlayWindowOptions(kind, options)); options.ensureOverlayWindowLevel(window); loadOverlayWindowLayer(window, kind); @@ -172,4 +148,5 @@ export function syncOverlayWindowLayer(window: BrowserWindow, layer: 'visible'): loadOverlayWindowLayer(window, layer); } +export { buildOverlayWindowOptions } from './overlay-window-options'; export type { OverlayWindowKind } from './overlay-window-input'; diff --git a/src/main.ts b/src/main.ts index e9541dd..86ca7de 100644 --- a/src/main.ts +++ b/src/main.ts @@ -376,6 +376,7 @@ import { createCharacterDictionaryAutoSyncRuntimeService } from './main/runtime/ import { notifyCharacterDictionaryAutoSyncStatus } from './main/runtime/character-dictionary-auto-sync-notifications'; import { createCurrentMediaTokenizationGate } from './main/runtime/current-media-tokenization-gate'; import { createStartupOsdSequencer } from './main/runtime/startup-osd-sequencer'; +import { formatSkippedYomitanWriteAction } from './main/runtime/yomitan-read-only-log'; import { getPreferredYomitanAnkiServerUrl as getPreferredYomitanAnkiServerUrlRuntime, shouldForceOverrideYomitanAnkiServer, @@ -1348,7 +1349,7 @@ const characterDictionaryAutoSyncRuntime = createCharacterDictionaryAutoSyncRunt }, importYomitanDictionary: async (zipPath) => { if (isYomitanExternalReadOnlyMode()) { - logSkippedYomitanWrite(`importYomitanDictionary(${zipPath})`); + logSkippedYomitanWrite(formatSkippedYomitanWriteAction('importYomitanDictionary', zipPath)); return false; } await ensureYomitanExtensionLoaded(); @@ -1359,7 +1360,9 @@ const characterDictionaryAutoSyncRuntime = createCharacterDictionaryAutoSyncRunt }, deleteYomitanDictionary: async (dictionaryTitle) => { if (isYomitanExternalReadOnlyMode()) { - logSkippedYomitanWrite(`deleteYomitanDictionary(${dictionaryTitle})`); + logSkippedYomitanWrite( + formatSkippedYomitanWriteAction('deleteYomitanDictionary', dictionaryTitle), + ); return false; } await ensureYomitanExtensionLoaded(); @@ -1370,7 +1373,9 @@ const characterDictionaryAutoSyncRuntime = createCharacterDictionaryAutoSyncRunt }, upsertYomitanDictionarySettings: async (dictionaryTitle, profileScope) => { if (isYomitanExternalReadOnlyMode()) { - logSkippedYomitanWrite(`upsertYomitanDictionarySettings(${dictionaryTitle})`); + logSkippedYomitanWrite( + formatSkippedYomitanWriteAction('upsertYomitanDictionarySettings', dictionaryTitle), + ); return false; } await ensureYomitanExtensionLoaded(); diff --git a/src/main/runtime/yomitan-read-only-log.test.ts b/src/main/runtime/yomitan-read-only-log.test.ts new file mode 100644 index 0000000..41ca286 --- /dev/null +++ b/src/main/runtime/yomitan-read-only-log.test.ts @@ -0,0 +1,24 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { formatSkippedYomitanWriteAction } from './yomitan-read-only-log'; + +test('formatSkippedYomitanWriteAction redacts full filesystem paths to basenames', () => { + assert.equal( + formatSkippedYomitanWriteAction('importYomitanDictionary', '/tmp/private/merged.zip'), + 'importYomitanDictionary(merged.zip)', + ); +}); + +test('formatSkippedYomitanWriteAction redacts dictionary titles', () => { + assert.equal( + formatSkippedYomitanWriteAction('deleteYomitanDictionary', 'SubMiner Character Dictionary'), + 'deleteYomitanDictionary()', + ); +}); + +test('formatSkippedYomitanWriteAction falls back when value is blank', () => { + assert.equal( + formatSkippedYomitanWriteAction('upsertYomitanDictionarySettings', ' '), + 'upsertYomitanDictionarySettings()', + ); +}); diff --git a/src/main/runtime/yomitan-read-only-log.ts b/src/main/runtime/yomitan-read-only-log.ts new file mode 100644 index 0000000..71816e5 --- /dev/null +++ b/src/main/runtime/yomitan-read-only-log.ts @@ -0,0 +1,25 @@ +import * as path from 'path'; + +function redactSkippedYomitanWriteValue( + actionName: 'importYomitanDictionary' | 'deleteYomitanDictionary' | 'upsertYomitanDictionarySettings', + rawValue: string, +): string { + const trimmed = rawValue.trim(); + if (!trimmed) { + return ''; + } + + if (actionName === 'importYomitanDictionary') { + const basename = path.basename(trimmed); + return basename || ''; + } + + return ''; +} + +export function formatSkippedYomitanWriteAction( + actionName: 'importYomitanDictionary' | 'deleteYomitanDictionary' | 'upsertYomitanDictionarySettings', + rawValue: string, +): string { + return `${actionName}(${redactSkippedYomitanWriteValue(actionName, rawValue)})`; +}