Automate AUR publish in tagged release workflow (#22)

This commit is contained in:
2026-03-14 19:49:46 -07:00
committed by GitHub
parent 99f4d2baaf
commit 9eed37420e
36 changed files with 641 additions and 722 deletions

View File

@@ -1195,14 +1195,32 @@ test('controller positive-number tuning rejects sub-unit values that floor to ze
const config = service.getConfig();
const warnings = service.getWarnings();
assert.equal(config.controller.scrollPixelsPerSecond, DEFAULT_CONFIG.controller.scrollPixelsPerSecond);
assert.equal(config.controller.horizontalJumpPixels, DEFAULT_CONFIG.controller.horizontalJumpPixels);
assert.equal(
config.controller.scrollPixelsPerSecond,
DEFAULT_CONFIG.controller.scrollPixelsPerSecond,
);
assert.equal(
config.controller.horizontalJumpPixels,
DEFAULT_CONFIG.controller.horizontalJumpPixels,
);
assert.equal(config.controller.repeatDelayMs, DEFAULT_CONFIG.controller.repeatDelayMs);
assert.equal(config.controller.repeatIntervalMs, DEFAULT_CONFIG.controller.repeatIntervalMs);
assert.equal(warnings.some((warning) => warning.path === 'controller.scrollPixelsPerSecond'), true);
assert.equal(warnings.some((warning) => warning.path === 'controller.horizontalJumpPixels'), true);
assert.equal(warnings.some((warning) => warning.path === 'controller.repeatDelayMs'), true);
assert.equal(warnings.some((warning) => warning.path === 'controller.repeatIntervalMs'), true);
assert.equal(
warnings.some((warning) => warning.path === 'controller.scrollPixelsPerSecond'),
true,
);
assert.equal(
warnings.some((warning) => warning.path === 'controller.horizontalJumpPixels'),
true,
);
assert.equal(
warnings.some((warning) => warning.path === 'controller.repeatDelayMs'),
true,
);
assert.equal(
warnings.some((warning) => warning.path === 'controller.repeatIntervalMs'),
true,
);
});
test('controller button index config rejects fractional values', () => {
@@ -1224,12 +1242,18 @@ test('controller button index config rejects fractional values', () => {
const config = service.getConfig();
const warnings = service.getWarnings();
assert.equal(config.controller.buttonIndices.select, DEFAULT_CONFIG.controller.buttonIndices.select);
assert.equal(
config.controller.buttonIndices.select,
DEFAULT_CONFIG.controller.buttonIndices.select,
);
assert.equal(
config.controller.buttonIndices.leftStickPress,
DEFAULT_CONFIG.controller.buttonIndices.leftStickPress,
);
assert.equal(warnings.some((warning) => warning.path === 'controller.buttonIndices.select'), true);
assert.equal(
warnings.some((warning) => warning.path === 'controller.buttonIndices.select'),
true,
);
assert.equal(
warnings.some((warning) => warning.path === 'controller.buttonIndices.leftStickPress'),
true,

View File

@@ -74,13 +74,15 @@ export function buildCoreConfigOptionRegistry(
kind: 'enum',
enumValues: ['auto', 'digital', 'analog'],
defaultValue: defaultConfig.controller.triggerInputMode,
description: 'How controller triggers are interpreted: auto, pressed-only, or thresholded analog.',
description:
'How controller triggers are interpreted: auto, pressed-only, or thresholded analog.',
},
{
path: 'controller.triggerDeadzone',
kind: 'number',
defaultValue: defaultConfig.controller.triggerDeadzone,
description: 'Minimum analog trigger value required when trigger input uses auto or analog mode.',
description:
'Minimum analog trigger value required when trigger input uses auto or analog mode.',
},
{
path: 'controller.repeatDelayMs',

View File

@@ -17,7 +17,12 @@ export function applyCoreDomainConfig(context: ResolveContext): void {
'leftTrigger',
'rightTrigger',
] as const;
const controllerAxisBindings = ['leftStickX', 'leftStickY', 'rightStickX', 'rightStickY'] as const;
const controllerAxisBindings = [
'leftStickX',
'leftStickY',
'rightStickX',
'rightStickY',
] as const;
if (isObject(src.texthooker)) {
const launchAtStartup = asBoolean(src.texthooker.launchAtStartup);
@@ -178,7 +183,12 @@ export function applyCoreDomainConfig(context: ResolveContext): void {
if (value !== undefined && Math.floor(value) > 0) {
resolved.controller[key] = Math.floor(value) as (typeof resolved.controller)[typeof key];
} else if (src.controller[key] !== undefined) {
warn(`controller.${key}`, src.controller[key], resolved.controller[key], 'Expected positive number.');
warn(
`controller.${key}`,
src.controller[key],
resolved.controller[key],
'Expected positive number.',
);
}
}
@@ -188,7 +198,12 @@ export function applyCoreDomainConfig(context: ResolveContext): void {
if (value !== undefined && value >= 0 && value <= 1) {
resolved.controller[key] = value as (typeof resolved.controller)[typeof key];
} else if (src.controller[key] !== undefined) {
warn(`controller.${key}`, src.controller[key], resolved.controller[key], 'Expected number between 0 and 1.');
warn(
`controller.${key}`,
src.controller[key],
resolved.controller[key],
'Expected number between 0 and 1.',
);
}
}

View File

@@ -467,16 +467,16 @@ test('registerIpcHandlers awaits saveControllerPreference through request-respon
const saveHandler = handlers.handle.get(IPC_CHANNELS.command.saveControllerPreference);
assert.ok(saveHandler);
await assert.rejects(
async () => {
await saveHandler!({}, { preferredGamepadId: 12 });
await assert.rejects(async () => {
await saveHandler!({}, { preferredGamepadId: 12 });
}, /Invalid controller preference payload/);
await saveHandler!(
{},
{
preferredGamepadId: 'pad-1',
preferredGamepadLabel: 'Pad 1',
},
/Invalid controller preference payload/,
);
await saveHandler!({}, {
preferredGamepadId: 'pad-1',
preferredGamepadLabel: 'Pad 1',
});
assert.deepEqual(controllerSaves, [
{
@@ -570,10 +570,7 @@ test('registerIpcHandlers rejects malformed controller preference payloads', asy
);
const saveHandler = handlers.handle.get(IPC_CHANNELS.command.saveControllerPreference);
await assert.rejects(
async () => {
await saveHandler!({}, { preferredGamepadId: 12 });
},
/Invalid controller preference payload/,
);
await assert.rejects(async () => {
await saveHandler!({}, { preferredGamepadId: 12 });
}, /Invalid controller preference payload/);
});

View File

@@ -265,13 +265,16 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
deps.saveSubtitlePosition(parsedPosition);
});
ipc.handle(IPC_CHANNELS.command.saveControllerPreference, async (_event: unknown, update: unknown) => {
const parsedUpdate = parseControllerPreferenceUpdate(update);
if (!parsedUpdate) {
throw new Error('Invalid controller preference payload');
}
await deps.saveControllerPreference(parsedUpdate);
});
ipc.handle(
IPC_CHANNELS.command.saveControllerPreference,
async (_event: unknown, update: unknown) => {
const parsedUpdate = parseControllerPreferenceUpdate(update);
if (!parsedUpdate) {
throw new Error('Invalid controller preference payload');
}
await deps.saveControllerPreference(parsedUpdate);
},
);
ipc.handle(IPC_CHANNELS.request.getMecabStatus, () => {
return deps.getMecabStatus();

View File

@@ -55,8 +55,9 @@ test('resolveExistingYomitanExtensionPath ignores source tree without built mani
test('resolveExternalYomitanExtensionPath returns external extension dir when manifest exists', () => {
const profilePath = path.join('/Users', 'kyle', '.local', 'share', 'gsm-profile');
const resolved = resolveExternalYomitanExtensionPath(profilePath, (candidate) =>
candidate === path.join(profilePath, 'extensions', 'yomitan', 'manifest.json'),
const resolved = resolveExternalYomitanExtensionPath(
profilePath,
(candidate) => candidate === path.join(profilePath, 'extensions', 'yomitan', 'manifest.json'),
);
assert.equal(resolved, path.join(profilePath, 'extensions', 'yomitan'));

View File

@@ -25,9 +25,7 @@ export function clearYomitanParserRuntimeState(deps: YomitanParserRuntimeStateDe
deps.setYomitanParserInitPromise(null);
}
export function clearYomitanExtensionRuntimeState(
deps: YomitanExtensionRuntimeStateDeps,
): void {
export function clearYomitanExtensionRuntimeState(deps: YomitanExtensionRuntimeStateDeps): void {
clearYomitanParserRuntimeState(deps);
deps.setYomitanExtension(null);
deps.setYomitanSession(null);

View File

@@ -694,7 +694,8 @@ const firstRunSetupService = createFirstRunSetupService({
});
return dictionaries.length;
},
isExternalYomitanConfigured: () => getResolvedConfig().yomitan.externalProfilePath.trim().length > 0,
isExternalYomitanConfigured: () =>
getResolvedConfig().yomitan.externalProfilePath.trim().length > 0,
detectPluginInstalled: () => {
const installPaths = resolveDefaultMpvInstallPaths(
process.platform,
@@ -3117,8 +3118,7 @@ function initializeOverlayRuntime(): void {
function openYomitanSettings(): boolean {
if (yomitanProfilePolicy.isExternalReadOnlyMode()) {
const message =
'Yomitan settings unavailable while using read-only external-profile mode.';
const message = 'Yomitan settings unavailable while using read-only external-profile mode.';
logger.warn(
'Yomitan settings window disabled while yomitan.externalProfilePath is configured because external profile mode is read-only.',
);
@@ -3572,11 +3572,11 @@ const { createMainWindow: createMainWindowHandler, createModalWindow: createModa
onRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
setOverlayDebugVisualizationEnabled: (enabled) =>
setOverlayDebugVisualizationEnabled(enabled),
isOverlayVisible: (windowKind) =>
windowKind === 'visible' ? overlayManager.getVisibleOverlayVisible() : false,
getYomitanSession: () => appState.yomitanSession,
tryHandleOverlayShortcutLocalFallback: (input) =>
overlayShortcutsRuntime.tryHandleOverlayShortcutLocalFallback(input),
isOverlayVisible: (windowKind) =>
windowKind === 'visible' ? overlayManager.getVisibleOverlayVisible() : false,
getYomitanSession: () => appState.yomitanSession,
tryHandleOverlayShortcutLocalFallback: (input) =>
overlayShortcutsRuntime.tryHandleOverlayShortcutLocalFallback(input),
forwardTabToMpv: () => sendMpvCommandRuntime(appState.mpvClient, ['keypress', 'TAB']),
onWindowClosed: (windowKind) => {
if (windowKind === 'visible') {
@@ -3704,12 +3704,7 @@ const { initializeOverlayRuntime: initializeOverlayRuntimeHandler } =
const { openYomitanSettings: openYomitanSettingsHandler } = createYomitanSettingsRuntime({
ensureYomitanExtensionLoaded: () => ensureYomitanExtensionLoaded(),
getYomitanSession: () => appState.yomitanSession,
openYomitanSettingsWindow: ({
yomitanExt,
getExistingWindow,
setWindow,
yomitanSession,
}) => {
openYomitanSettingsWindow: ({ yomitanExt, getExistingWindow, setWindow, yomitanSession }) => {
openYomitanSettingsWindow({
yomitanExt: yomitanExt as Extension,
getExistingWindow: () => getExistingWindow() as BrowserWindow | null,

View File

@@ -279,7 +279,11 @@ export function createFirstRunSetupService(deps: {
});
if (
isSetupCompleted(state) &&
!(state.yomitanSetupMode === 'external' && !externalYomitanConfigured && !yomitanSetupSatisfied)
!(
state.yomitanSetupMode === 'external' &&
!externalYomitanConfigured &&
!yomitanSetupSatisfied
)
) {
completed = true;
return refreshWithState(state);

View File

@@ -1,7 +1,10 @@
import * as path from 'path';
function redactSkippedYomitanWriteValue(
actionName: 'importYomitanDictionary' | 'deleteYomitanDictionary' | 'upsertYomitanDictionarySettings',
actionName:
| 'importYomitanDictionary'
| 'deleteYomitanDictionary'
| 'upsertYomitanDictionarySettings',
rawValue: string,
): string {
const trimmed = rawValue.trim();
@@ -18,7 +21,10 @@ function redactSkippedYomitanWriteValue(
}
export function formatSkippedYomitanWriteAction(
actionName: 'importYomitanDictionary' | 'deleteYomitanDictionary' | 'upsertYomitanDictionarySettings',
actionName:
| 'importYomitanDictionary'
| 'deleteYomitanDictionary'
| 'upsertYomitanDictionarySettings',
rawValue: string,
): string {
return `${actionName}(${redactSkippedYomitanWriteValue(actionName, rawValue)})`;

View File

@@ -9,7 +9,11 @@ test('yomitan settings runtime composes opener with built deps', async () => {
const runtime = createYomitanSettingsRuntime({
ensureYomitanExtensionLoaded: async () => ({ id: 'ext' }),
openYomitanSettingsWindow: ({ getExistingWindow, setWindow, yomitanSession: forwardedSession }) => {
openYomitanSettingsWindow: ({
getExistingWindow,
setWindow,
yomitanSession: forwardedSession,
}) => {
calls.push(`open-window:${(forwardedSession as { id: string } | null)?.id ?? 'null'}`);
const current = getExistingWindow();
if (!current) {
@@ -54,5 +58,7 @@ test('yomitan settings runtime warns and does not open when no yomitan session i
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(existingWindow, null);
assert.deepEqual(calls, ['warn:Unable to open Yomitan settings: Yomitan session is unavailable.']);
assert.deepEqual(calls, [
'warn:Unable to open Yomitan settings: Yomitan session is unavailable.',
]);
});

View File

@@ -67,6 +67,25 @@ test('windows release workflow publishes unsigned artifacts directly without Sig
assert.ok(!releaseWorkflow.includes('SIGNPATH_'));
});
test('release workflow publishes subminer-bin to AUR from tagged release artifacts', () => {
assert.match(releaseWorkflow, /aur-publish:/);
assert.match(releaseWorkflow, /needs:\s*\[release\]/);
assert.match(releaseWorkflow, /AUR_SSH_PRIVATE_KEY/);
assert.match(releaseWorkflow, /ssh:\/\/aur@aur\.archlinux\.org\/subminer-bin\.git/);
assert.match(releaseWorkflow, /Install makepkg/);
assert.match(releaseWorkflow, /scripts\/update-aur-package\.sh/);
assert.match(releaseWorkflow, /version_no_v="\$\{\{ steps\.version\.outputs\.VERSION \}\}"/);
assert.match(releaseWorkflow, /SubMiner-\$\{version_no_v\}\.AppImage/);
assert.doesNotMatch(
releaseWorkflow,
/SubMiner-\$\{\{ steps\.version\.outputs\.VERSION \}\}\.AppImage/,
);
});
test('release workflow skips empty AUR sync commits', () => {
assert.match(releaseWorkflow, /if git diff --quiet -- PKGBUILD \.SRCINFO; then/);
});
test('Makefile routes Windows install-plugin setup through bun and documents Windows builds', () => {
assert.match(
makefile,

View File

@@ -25,20 +25,17 @@ test('controller status indicator shows once when a controller is first detected
classList,
};
const indicator = createControllerStatusIndicator(
{ controllerStatusToast: toast } as never,
{
durationMs: 1500,
setTimeout: (callback: () => void) => {
const id = nextTimerId++;
scheduled.set(id, callback);
return id as never;
},
clearTimeout: (id) => {
scheduled.delete(id as never as number);
},
const indicator = createControllerStatusIndicator({ controllerStatusToast: toast } as never, {
durationMs: 1500,
setTimeout: (callback: () => void) => {
const id = nextTimerId++;
scheduled.set(id, callback);
return id as never;
},
);
clearTimeout: (id) => {
scheduled.delete(id as never as number);
},
});
indicator.update({
connectedGamepads: [],
@@ -78,13 +75,10 @@ test('controller status indicator announces newly detected controllers after sta
classList: createClassList(['hidden']),
};
const indicator = createControllerStatusIndicator(
{ controllerStatusToast: toast } as never,
{
setTimeout: () => 1 as never,
clearTimeout: () => {},
},
);
const indicator = createControllerStatusIndicator({ controllerStatusToast: toast } as never, {
setTimeout: () => 1 as never,
clearTimeout: () => {},
});
indicator.update({
connectedGamepads: [{ id: 'pad-1', index: 0, mapping: 'standard', connected: true }],

View File

@@ -58,7 +58,9 @@ export function createControllerStatusIndicator(
(device) => device.id === snapshot.activeGamepadId,
);
const announcedDevice =
newDevices.find((device) => device.id === snapshot.activeGamepadId) ?? newDevices[0] ?? activeDevice;
newDevices.find((device) => device.id === snapshot.activeGamepadId) ??
newDevices[0] ??
activeDevice;
show(`Controller detected: ${getDeviceLabel(announcedDevice)}`);
}

View File

@@ -39,8 +39,11 @@ function createControllerConfig(
buttonIndices?: Partial<ResolvedControllerConfig['buttonIndices']>;
} = {},
): ResolvedControllerConfig {
const { bindings: bindingOverrides, buttonIndices: buttonIndexOverrides, ...restOverrides } =
overrides;
const {
bindings: bindingOverrides,
buttonIndices: buttonIndexOverrides,
...restOverrides
} = overrides;
return {
enabled: true,
preferredGamepadId: '',
@@ -90,7 +93,11 @@ function createControllerConfig(
test('gamepad controller selects the first connected controller by default', () => {
const updates: string[] = [];
const controller = createGamepadController({
getGamepads: () => [null, createGamepad('pad-2', { index: 1 }), createGamepad('pad-3', { index: 2 })],
getGamepads: () => [
null,
createGamepad('pad-2', { index: 1 }),
createGamepad('pad-3', { index: 2 }),
],
getConfig: () => createControllerConfig(),
getKeyboardModeEnabled: () => false,
getLookupWindowOpen: () => false,
@@ -310,13 +317,12 @@ test('gamepad controller maps L1 play-current, R1 next-audio, and popup navigati
buttons[7] = { value: 0.9, pressed: true, touched: true };
const controller = createGamepadController({
getGamepads: () =>
[
createGamepad('pad-1', {
axes: [0, -0.75, 0.1, 0, 0.8],
buttons,
}),
],
getGamepads: () => [
createGamepad('pad-1', {
axes: [0, -0.75, 0.1, 0, 0.8],
buttons,
}),
],
getConfig: () =>
createControllerConfig({
bindings: {
@@ -352,7 +358,10 @@ test('gamepad controller maps L1 play-current, R1 next-audio, and popup navigati
assert.equal(calls.includes('prev-audio'), false);
assert.equal(calls.includes('toggle-mpv-pause'), true);
assert.equal(calls.includes('quit-mpv'), true);
assert.deepEqual(scrollCalls.map((value) => Math.round(value)), [-67]);
assert.deepEqual(
scrollCalls.map((value) => Math.round(value)),
[-67],
);
assert.equal(calls.includes('jump:160'), true);
});
@@ -492,7 +501,10 @@ test('gamepad controller maps d-pad left/right to selection and d-pad up/down to
controller.poll(100);
assert.deepEqual(selectionCalls, [1]);
assert.deepEqual(scrollCalls.map((value) => Math.round(value)), [-90]);
assert.deepEqual(
scrollCalls.map((value) => Math.round(value)),
[-90],
);
});
test('gamepad controller maps d-pad axes 6 and 7 to selection and popup scroll', () => {
@@ -524,7 +536,10 @@ test('gamepad controller maps d-pad axes 6 and 7 to selection and popup scroll',
controller.poll(100);
assert.deepEqual(selectionCalls, [1]);
assert.deepEqual(scrollCalls.map((value) => Math.round(value)), [-90]);
assert.deepEqual(
scrollCalls.map((value) => Math.round(value)),
[-90],
);
});
test('gamepad controller trigger analog mode uses trigger values above threshold', () => {

View File

@@ -159,10 +159,7 @@ function resolveDpadValue(
);
}
function resolveDpadAxisValue(
gamepad: GamepadLike,
axisIndex: number,
): number {
function resolveDpadAxisValue(gamepad: GamepadLike, axisIndex: number): number {
const value = resolveGamepadAxis(gamepad, axisIndex);
if (Math.abs(value) < 0.5) {
return 0;
@@ -175,7 +172,12 @@ function resolveDpadHorizontalValue(gamepad: GamepadLike, triggerDeadzone: numbe
if (axisValue !== 0) {
return axisValue;
}
return resolveDpadValue(gamepad, DPAD_BUTTON_INDEX.left, DPAD_BUTTON_INDEX.right, triggerDeadzone);
return resolveDpadValue(
gamepad,
DPAD_BUTTON_INDEX.left,
DPAD_BUTTON_INDEX.right,
triggerDeadzone,
);
}
function resolveDpadVerticalValue(gamepad: GamepadLike, triggerDeadzone: number): number {
@@ -201,7 +203,12 @@ function createHoldState(): HoldState {
};
}
function shouldFireHeldAction(state: HoldState, now: number, repeatDelayMs: number, repeatIntervalMs: number): boolean {
function shouldFireHeldAction(
state: HoldState,
now: number,
repeatDelayMs: number,
repeatIntervalMs: number,
): boolean {
if (!state.initialFired) {
state.initialFired = true;
state.lastFireAt = now;
@@ -305,11 +312,7 @@ export function createGamepadController(options: GamepadControllerOptions) {
}
}
function handleSelectionAxis(
value: number,
now: number,
config: ResolvedControllerConfig,
): void {
function handleSelectionAxis(value: number, now: number, config: ResolvedControllerConfig): void {
const activationThreshold = Math.max(config.stickDeadzone, 0.55);
if (Math.abs(value) < activationThreshold) {
resetHeldAction(selectionHold);
@@ -327,11 +330,7 @@ export function createGamepadController(options: GamepadControllerOptions) {
}
}
function handleJumpAxis(
value: number,
now: number,
config: ResolvedControllerConfig,
): void {
function handleJumpAxis(value: number, now: number, config: ResolvedControllerConfig): void {
const activationThreshold = Math.max(config.stickDeadzone, 0.55);
if (Math.abs(value) < activationThreshold) {
resetHeldAction(jumpHold);
@@ -418,9 +417,7 @@ export function createGamepadController(options: GamepadControllerOptions) {
}
const interactionAllowed =
config.enabled &&
options.getKeyboardModeEnabled() &&
!options.getInteractionBlocked();
config.enabled && options.getKeyboardModeEnabled() && !options.getInteractionBlocked();
if (config.enabled) {
handleButtonEdge(
config.bindings.toggleKeyboardOnlyMode,

View File

@@ -3,10 +3,7 @@ import test from 'node:test';
import { createKeyboardHandlers } from './keyboard.js';
import { createRendererState } from '../state.js';
import {
YOMITAN_POPUP_COMMAND_EVENT,
YOMITAN_POPUP_HIDDEN_EVENT,
} from '../yomitan-popup.js';
import { YOMITAN_POPUP_COMMAND_EVENT, YOMITAN_POPUP_HIDDEN_EVENT } from '../yomitan-popup.js';
type CommandEventDetail = {
type?: string;
@@ -478,14 +475,11 @@ test('keyboard mode: controller helpers dispatch popup audio play/cycle and scro
assert.equal(handlers.cyclePopupAudioSourceForController(1), true);
assert.equal(handlers.scrollPopupByController(48, -24), true);
assert.deepEqual(
testGlobals.commandEvents.slice(-3),
[
{ type: 'playCurrentAudio' },
{ type: 'cycleAudioSource', direction: 1 },
{ type: 'scrollBy', deltaX: 48, deltaY: -24 },
],
);
assert.deepEqual(testGlobals.commandEvents.slice(-3), [
{ type: 'playCurrentAudio' },
{ type: 'cycleAudioSource', direction: 1 },
{ type: 'scrollBy', deltaX: 48, deltaY: -24 },
]);
} finally {
testGlobals.restore();
}
@@ -531,7 +525,8 @@ test('keyboard mode: Alt+Shift+C opens controller debug modal even while popup i
});
test('keyboard mode: controller select modal handles arrow keys before yomitan popup', async () => {
const { ctx, testGlobals, handlers, controllerSelectKeydownCount } = createKeyboardHandlerHarness();
const { ctx, testGlobals, handlers, controllerSelectKeydownCount } =
createKeyboardHandlerHarness();
try {
await handlers.setupMpvInputForwarding();

View File

@@ -187,7 +187,9 @@ export function createKeyboardHandlers(
);
}
function clearKeyboardSelectedWordClasses(wordNodes: HTMLElement[] = getSubtitleWordNodes()): void {
function clearKeyboardSelectedWordClasses(
wordNodes: HTMLElement[] = getSubtitleWordNodes(),
): void {
for (const wordNode of wordNodes) {
wordNode.classList.remove(KEYBOARD_SELECTED_WORD_CLASS);
}

View File

@@ -106,7 +106,10 @@ test('controller debug modal renders active controller axes, buttons, and config
assert.match(ctx.dom.controllerDebugStatus.textContent, /pad-1/);
assert.match(ctx.dom.controllerDebugSummary.textContent, /standard/);
assert.match(ctx.dom.controllerDebugAxes.textContent, /axis\[0\] = 0\.500/);
assert.match(ctx.dom.controllerDebugButtons.textContent, /button\[0\] value=1\.000 pressed=true/);
assert.match(
ctx.dom.controllerDebugButtons.textContent,
/button\[0\] value=1\.000 pressed=true/,
);
assert.match(ctx.dom.controllerDebugButtonIndices.textContent, /"buttonIndices": \{/);
assert.match(ctx.dom.controllerDebugButtonIndices.textContent, /"select": 6/);
assert.match(ctx.dom.controllerDebugButtonIndices.textContent, /"leftStickPress": 9/);
@@ -224,8 +227,14 @@ test('controller debug modal copies buttonIndices config to clipboard', async ()
await new Promise((resolve) => setTimeout(resolve, 0));
assert.deepEqual(copied, [ctx.dom.controllerDebugButtonIndices.textContent]);
assert.match(ctx.dom.controllerDebugStatus.textContent, /Copied controller buttonIndices config/);
assert.match(ctx.dom.controllerDebugToast.textContent, /Copied controller buttonIndices config/);
assert.match(
ctx.dom.controllerDebugStatus.textContent,
/Copied controller buttonIndices config/,
);
assert.match(
ctx.dom.controllerDebugToast.textContent,
/Copied controller buttonIndices config/,
);
assert.equal(ctx.dom.controllerDebugToast.classList.contains('hidden'), false);
} finally {
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });

View File

@@ -18,21 +18,19 @@ function formatButtons(
}
function formatButtonIndices(
value:
| {
select: number;
buttonSouth: number;
buttonEast: number;
buttonNorth: number;
buttonWest: number;
leftShoulder: number;
rightShoulder: number;
leftStickPress: number;
rightStickPress: number;
leftTrigger: number;
rightTrigger: number;
}
| null,
value: {
select: number;
buttonSouth: number;
buttonEast: number;
buttonNorth: number;
buttonWest: number;
leftShoulder: number;
rightShoulder: number;
leftStickPress: number;
rightStickPress: number;
leftTrigger: number;
rightTrigger: number;
} | null,
): string {
if (!value) {
return 'No controller config loaded.';
@@ -97,7 +95,9 @@ export function createControllerDebugModal(
);
setStatus(
activeDevice?.id ??
(ctx.state.connectedGamepads.length > 0 ? 'Controller connected.' : 'No controller detected.'),
(ctx.state.connectedGamepads.length > 0
? 'Controller connected.'
: 'No controller detected.'),
);
ctx.dom.controllerDebugSummary.textContent =
ctx.state.connectedGamepads.length > 0

View File

@@ -45,7 +45,9 @@ export function createControllerSelectModal(
syncSelectedControllerId();
return;
}
const preferredIndex = ctx.state.connectedGamepads.findIndex((device) => device.id === preferredId);
const preferredIndex = ctx.state.connectedGamepads.findIndex(
(device) => device.id === preferredId,
);
if (preferredIndex >= 0) {
ctx.state.controllerDeviceSelectedIndex = preferredIndex;
syncSelectedControllerId();

View File

@@ -63,8 +63,7 @@ body {
padding: 8px 12px;
border-radius: 999px;
border: 1px solid rgba(138, 213, 202, 0.45);
background:
linear-gradient(135deg, rgba(10, 44, 40, 0.94), rgba(8, 28, 33, 0.94));
background: linear-gradient(135deg, rgba(10, 44, 40, 0.94), rgba(8, 28, 33, 0.94));
color: rgba(228, 255, 251, 0.98);
font-size: 12px;
font-weight: 700;

View File

@@ -166,7 +166,9 @@ export function resolveRendererDom(): RendererDom {
controllerDebugSummary: getRequiredElement<HTMLDivElement>('controllerDebugSummary'),
controllerDebugAxes: getRequiredElement<HTMLPreElement>('controllerDebugAxes'),
controllerDebugButtons: getRequiredElement<HTMLPreElement>('controllerDebugButtons'),
controllerDebugButtonIndices: getRequiredElement<HTMLPreElement>('controllerDebugButtonIndices'),
controllerDebugButtonIndices: getRequiredElement<HTMLPreElement>(
'controllerDebugButtonIndices',
),
sessionHelpModal: getRequiredElement<HTMLDivElement>('sessionHelpModal'),
sessionHelpClose: getRequiredElement<HTMLButtonElement>('sessionHelpClose'),

View File

@@ -223,7 +223,10 @@ export function ensureDefaultConfigBootstrap(options: {
const mkdirSync = options.mkdirSync ?? fs.mkdirSync;
const writeFileSync = options.writeFileSync ?? fs.writeFileSync;
if (existsSync(options.configFilePaths.jsoncPath) || existsSync(options.configFilePaths.jsonPath)) {
if (
existsSync(options.configFilePaths.jsoncPath) ||
existsSync(options.configFilePaths.jsonPath)
) {
return;
}